<template>
  <!-- 钢琴块主体 -->
  <ul ref="piano-group" class="piano-group"></ul>
</template>


<script setup lang="ts">
  // IProps接口：描述props
  interface IProps {
    whiteWidth?: number;           // 白键宽度
    whiteHeight?: number;          // 白键高度
    blackWidth?: number;           // 黑键宽度
    blackHeight?: number;          // 黑键高度
    gap?: number;                  // 间距
    mapping?: TPinaoMapping;       // 琴键映射
    fontSize?: number;             // 音名文字大小
    numFontSize?: number;          // 唱名文字大小
    numFontColor?: string;         // 唱名文字颜色
    marginBottom?: number;         // 音名距离白色琴键底部距离

    major: string;                 // 调式
    index: number;                 // 标识是哪个组
  }

  // TPinaoMapping：描述琴键的映射
  type TPinaoMapping = string[];

  // IMaping：描述震动频率的映射
  interface IMapping {
    [key: string]: number;
  };

  // TDotState：描述是低音点还是高音点
  type TDotState = 'low' | 'normal' | 'high';

  // TMusicNum：描述简谱的7个音
  type TMusicNum = 1 | 2 | 3 | 4 | 5 | 6 | 7;

  import { useTemplateRef } from 'vue';
  import { onMounted } from 'vue';
  
  const pianoGroup = useTemplateRef('piano-group');

  const { 
    whiteWidth = 60, 
    whiteHeight = 300, 
    blackWidth = 46, 
    blackHeight = 200,
    gap = 4,
    mapping = [
      'C4', '#C4', 'D4', '#D4', 'E4', 'F4',
      '#F4', 'G4', '#G4', 'A4', '#A4', 'B4',
    ],
    fontSize = 18,
    numFontSize = 18,
    marginBottom = 10,
  } = defineProps<IProps>();

  // 音频上下文
  const ctx = new AudioContext();

  // 频率映射表
  const frequencyMapping: IMapping = {
    // 小字组
    'C3': 130.81,
    '#C3': 138.59,
    'D3': 146.83,
    '#D3': 155.56,
    'E3': 164.81,
    'F3': 174.61,
    '#F3': 184.99,
    'G3': 195.99,
    '#G3': 207.65,
    'A3': 220.00,
    '#A3': 233.08,
    'B3': 246.94,

    // 小字1组
    'C4': 261.63,
    '#C4': 277.18,
    'D4': 293.66,
    '#D4': 311.13,
    'E4': 329.63,
    'F4': 349.23,
    '#F4': 369.99,
    'G4': 391.99,
    '#G4': 415.30,
    'A4': 440.00,
    '#A4': 466.16,
    'B4': 493.88,

    // 小字2组
    'C5': 523.25,
    '#C5': 554.37,
    'D5': 587.33,
    '#D5': 622.25,
    'E5': 659.25,
    'F5': 698.46,
    '#F5': 739.99,
    'G5': 783.99,
    '#G5': 830.61,
    'A5': 880.00,
    '#A5': 932.33,
    'B5': 987.77,
  };



  onMounted(() => {
    createPianoGroup();
  });



  // 绘制钢琴组
  function createPianoGroup() {
    const piano = pianoGroup.value!;
    piano.style.width = 'fit-content';
    piano.style.position = 'relative';
    piano.style.backgroundColor = '#000';
    piano.style.display = 'flex';
    piano.style.gap = `${gap}px`;

    const whiteMapping = mapping.filter(el => !el.startsWith('#'));
    const blackMapping = mapping.filter(el => el.startsWith('#'));

    // 排版白键
    for (let i=0; i<7; i++) {
      const item = document.createElement('li');
      const musicChar = whiteMapping[i];
      item.style.width = `${whiteWidth}px`;
      item.style.height = `${whiteHeight}px`;
      item.style.backgroundColor = '#fff';
      item.style.cursor = 'pointer';
      item.style.userSelect = 'none';
      item.style.display = 'flex';
      item.style.flexDirection = 'column-reverse';
      item.style.alignItems = 'center';

      // 绘制音名
      const span1 = document.createElement('span');
      span1.textContent = whiteMapping[i];
      span1.style.fontSize = `${fontSize}px`;
      span1.style.color = 'black';
      span1.style.marginBottom = `${marginBottom}px`;
      item.appendChild(span1);


      // 绘制唱名
      if (i == 1) {
        const ele = createMusicDot(5, 'high', numFontSize);
        ele.style.marginBottom = '20px';
        item.appendChild(ele);
      }

      piano.appendChild(item);

      // 绑定点击事件
      item.addEventListener('click', () => {

        // 点击效果消失延迟
        item.classList.add('active');
        setTimeout(() => {
          item.classList.remove('active');
        }, 120);


        // 播放音频
        playSound(ctx, frequencyMapping[musicChar], 180);
      });
    }

    // 排版黑键
    const spacing  = whiteWidth - (blackWidth - gap) / 2;
    let offset = 0;
    let left;
    for (let i=0; i<5; i++) {
      const item = document.createElement('li');
      item.style.width = `${blackWidth}px`;
      item.style.height = `${blackHeight}px`;
      item.style.backgroundColor = '#000';
      item.style.cursor = 'pointer';
      item.style.userSelect = 'none';
      item.style.display = 'flex';
      item.style.justifyContent = 'center';

      // 计算left值
      item.style.position = 'absolute';
      left = offset + spacing;
      offset += whiteWidth + gap;       // 为下一次做准备
      if (i===1) {
        offset += whiteWidth + gap;     // E与F之间没有黑键
      }

      item.style.left = `${left}px`;
      item.style.top = '0px';

      piano.appendChild(item);

      // 绑定点击事件
      const musicChar = blackMapping[i];
      item.addEventListener('click', () => {
        // 点击效果消失延迟
        item.classList.add('active');
        setTimeout(() => {
          item.classList.remove('active');
        }, 120);

        // 播放音频
        playSound(ctx, frequencyMapping[musicChar], 180);

      });

    }

  }

  // 绘制倍音点
  // num {enum} 描述当前绘制的音是哪一个
  // flag {enum} 'normal' | 'low' | 'high' 描述音是高音还是低音
  // fontSize {number} 字体大小
  function createMusicDot(num: TMusicNum, flag: TDotState, fontSize: number): HTMLDivElement {
    const div = document.createElement('div');        // 将来需要返回的元素
    const text = document.createElement('span');      // 写入文字
    text.textContent = num.toString();
    div.style.display = 'flex';
    div.style.flexDirection = 'column';
    div.style.alignItems = 'center';
    div.style.color = 'red';
    div.style.fontSize = `${fontSize}px`;
    div.style.fontWeight = 'bold';
    div.appendChild(text);

    if (flag === 'normal') {
      return div;
    }

    const size = 5;                                   // 着重点的尺寸（直径）

    if (flag === 'high') {
      div.style.flexDirection = 'column-reverse';
    }
    // 绘制着重点
    const dot = document.createElement('span');
    // dot.style.display = 'block';
    dot.style.width = `${size}px`;
    dot.style.height = `${size}px`;
    dot.style.borderRadius = '50%';
    dot.style.backgroundColor = 'red';
    div.appendChild(dot);
    return div;
  }

  // 播放音频
  function playSound(ctx: AudioContext, frequency: number, duration: number) {
      // 1. 音频上下文ctx
      // 2. 振荡器
      const oscillator = ctx.createOscillator();
      // 2.1 设置声音频率
      oscillator.frequency.setValueAtTime(frequency, ctx.currentTime);
      // 2.2 设置声音音色
      oscillator.type = 'square';                 
      // 3. 增益节点
      const gainNode = ctx.createGain();
      // 3.1 设置音量
      gainNode.gain.setValueAtTime(0, ctx.currentTime);
      gainNode.gain.linearRampToValueAtTime(1, ctx.currentTime+0.02);
      // 4. 连接
      oscillator.connect(gainNode); 
      gainNode.connect(ctx.destination);                  // 4.1 增益节点连接外部音频设备
      // 5. 播放
      oscillator.start();
      // 6. 停止
      setTimeout(() => {
          oscillator.stop();
          oscillator.disconnect();
          gainNode.disconnect();
      }, duration);
  }


</script>

<style lang="scss">
  @use "sass:color";

  .piano-group {
    li {
      &:active,
      &.active {
        $color: color.adjust(#0f0, $lightness: -20%);
        background-color: $color !important;
        span {
          color: yellow !important;
        }
      }

    }
  }
</style>